使用 TSX 开发游戏
欢迎来到 Dora SSR 游戏开发的世界!如果你是一个前端开发者,或者对 TypeScript 和 React 有一定了解,那么你会发现使用 TSX 编写游戏是一个令人兴奋且熟悉的体验。如果不了解也完全不用担心,本教程将带你从零开始,了解如何使用 Dora SSR 和 TSX 开发游戏,并介绍一些 TSX 的基础概念。
1. 什么是 TSX?
1.1 TSX 基础概念
TSX 是 TypeScript 与 JSX 的结合,它允许你在 TypeScript 中使用类似 HTML 的标签语法来构建界面和组件。这在 React 开发中非常常见,而在 Dora SSR 中,你可以使用 TSX 来定义游戏对象和场景。(注:JSX 是 JavaScript 的语法扩展,允许你在 JavaScript 中编写类似 XML 的代码。)
要在 Dora SSR 中使用 TSX,你需要注意在 Web IDE 中创建代码文件时,选择 TypeScript 语言以及 .tsx
扩展名。
TSX 标签和属性:在 TSX 中,你可以像在 HTML 中一样使用标签和属性。例如:
<sprite file="Image/logo.png" scaleX={0.2} scaleY={0.2}/>
<sprite>
是一个标签,表示游戏中的精灵(图像)。file
、scaleX
、scaleY
是属性,用于设置精灵的文件路径和缩放比例。
TSX 函数组件:你可以创建函数组件,接受属性并返回 TSX 元素。这与 React 中的函数组件类似。
interface ItemProps {
x?: number;
y?: number;
children?: any;
}
const Item = (props: ItemProps) => {
return (
<node x={props.x} y={props.y}>
{/* 子元素 */}
{props.children}
</node>
);
};
2. 使用 TSX 编写游戏场景
2.1 创建简单的游戏对象
在 Dora SSR 中,你可以使用 TSX 标签来定义游戏对象。例如,创建一个精灵:
<sprite file="Image/logo.png" scaleX={0.2} scaleY={0.2}/>
2.2 转换为渲染对象实例
要将上述 TSX 标签转换为可渲染的游戏对象,需要使用 toNode()
函数。这个函数接受一个 TSX 元素或元素数组,返回对应的游戏节点。
import { React, toNode } from 'DoraX';
const node = toNode(<sprite file="Image/logo.png" scaleX={0.2} scaleY={0.2}/>);
这样,node
就是一个实例化的游戏场景节点对象。
3. 创建 TSX 函数组件
函数组件使你的代码更具可重用性和可读性。以下是如何在 Dora SSR 中创建一个简单的盒子组件。
import { React, toNode } from 'DoraX';
interface BoxProps {
x?: number;
y?: number;
color?: number;
}
const Box = (props: BoxProps) => {
return (
<draw-node x={props.x ?? 0} y={props.y ?? 0}>
<rect-shape width={100} height={100} fillColor={props.color || 0xffffffff}/>
</draw-node>
);
};
在上面的示例中,Box
是一个函数组件,接受 x
、y
和 color
属性,返回一个带有矩形形状的绘制节点。
使用组件:
const boxes = [
<Box x={0} y={0} color={0xffff0000}/>,
<Box x={150} y={0} color={0xff00ff00}/>,
<Box x={300} y={0} color={0xff0000ff}/>,
];
const scene = toNode(boxes);
在上面的示例中,我们创建了三个不同颜色的盒子,并将它们放在一个数组中,然后将数组传递给 toNode()
函数,实例化为游戏场景节点。
4. 使用 useRef 获取实例化对象
在游戏开发中,你可能需要直接操作某个游戏对象,例如改变其位置、旋转或响应事件。useRef()
函数可以帮助你获取 TSX 元素对应的实例化对象。
import { React, useRef, toNode } from 'DoraX';
import { Body, BodyMoveType, Vec2 } from 'Dora';
const boxRef = useRef<Body.Type>();
const MovableBox = () => {
return (
<body ref={boxRef} x={0} y={0} type={BodyMoveType.Dynamic}>
<rect-fixture width={100} height={100}/>
<draw-node>
<rect-shape width={100} height={100} fillColor={0xffffffff}/>
</draw-node>
</body>
);
};
const scene = toNode(
<physics-world>
<MovableBox/>
</physics-world>
);
// 现在,你可以通过 boxRef.current 操作实例化的对象
if (boxRef.current) {
boxRef.current.position = Vec2(200, 200);
}
在上面的示例中,boxRef
是一个引用,指向 <body>
标签实例化后的对象。你可以通过 boxRef.current
来访问并操作该对象。
5. 创建 class 组件
除了函数组件,你还可以创建 class 形式的组件。class 组件可以包含状态和生命周期方法,使你能够更灵活地管理组件的行为。不过,class 组件的使用相对更为复杂,建议优先使用函数组件。
import { React, toNode, useRef } from 'DoraX';
import { Label } from 'Dora';
// 定义计数器组件的初始化属性
interface CounterProps {
count: number;
}
// 创建一个计数器组件,必须继承 React.Component
class Counter extends React.Component<CounterProps> {
count: number;
labelRef: JSX.Ref<Label.Type>;
// 构造函数,用于接受初始化属性
constructor(props: CounterProps) {
super(props);
this.count = props.count;
this.labelRef = useRef<Label.Type>();
}
// 渲染函数,返回组件的 TSX 元素
render() {
return (
<label ref={this.labelRef} text={this.count.toString()}
fontName='sarasa-mono-sc-regular' fontSize={80}
onTapped={this.onTapped}/>
);
}
// 点击事件处理函数
onTapped = () => {
if (this.labelRef.current) {
this.labelRef.current.text = (++this.count).toString();
}
};
}
// 实例化计数器组件
toNode(<Counter count={1}/>);
在上面的示例中,我们创建了一个计数器组件 Counter
,它包含一个计数值和一个标签。当标签被点击时,计数值会增加,并更新标签的文本。
6. 完整示例:创建一个简单的小游戏
让我们综合以上内容,创建一个简单的小游戏,包含以下要素:
- 一个可移动的角色(精灵)
- 一些静态的障碍物(盒子)
- 点击屏幕控制角色移动
6.1 定义角色组件
import { React, useRef, toNode } from 'DoraX';
import { Body, BodyMoveType, Vec2 } from 'Dora';
const playerRef = useRef<Body.Type>();
const Player = () => {
return (
<body ref={playerRef} x={0} y={0} type={BodyMoveType.Dynamic}
linearAcceleration={Vec2.zero} linearDamping={1}>
<rect-fixture width={50} height={50}/>
<draw-node>
<rect-shape width={50} height={50} fillColor={0xff00ff00}/>
</draw-node>
</body>
);
};
在上面的代码中,Player
是一个角色组件,包含一个可移动的矩形刚体。playerRef
是一个引用,用于获取实例化的角色对象。
6.2 定义障碍物组件
const Obstacle = (props: {x: number; y: number}) => {
return (
<body type={BodyMoveType.Static} x={props.x} y={props.y}>
<rect-fixture width={100} height={100}/>
<draw-node>
<rect-shape width={100} height={100} fillColor={0xffff0000}/>
</draw-node>
</body>
);
};
Obstacle
是一个障碍物组件,包含一个静态的矩形刚体。你可以通过传入 x
和 y
属性来设置障碍物的位置。
6.3 创建游戏场景
const GameScene = () => {
return (
<physics-world
onTapBegan={touch => {
// 控制角色移动到点击位置
const {current: player} = playerRef;
if (player) {
player.velocity = touch.location
.sub(player.position)
.normalize()
.mul(300);
}
}}>
<Player/>
<Obstacle x={200} y={0}/>
<Obstacle x={-200} y={0}/>
</physics-world>
);
};
在 GameScene
组件中,我们创建了一个物理世界,监听点击事件,并控制角色向点击位置移动。同时,添加了两个障碍物。请注意我们的物理刚体都必须作为子节点放在 physics-world
组件中。
6.4 运行游戏
将场景节点进行实例化:
const scene = toNode(<GameScene/>);
现在,你已经创建了一个简单的小游戏,角色可以在屏幕上移动,并与障碍物进行交互。
7. 一些特殊的 TSX 元素的用法
在 Dora SSR 中,有一些特殊的 TSX 元素,可以提供创建场景节点以外的功能。
7.1 动作元素
动作元素用于执行一系列动作,例如移动、旋转、缩放等。你可以在 TSX 中使用动作元素,将其作为子元素添加到游戏对象中。
<sprite file="Image/logo.png">
<move time={0.5} startX={0} startY={0} stopX={200} stopY={200}/>
</sprite>
在上面的示例中,我们创建了一个精灵,并添加了一个移动动作,使其从 (0, 0)
移动到 (200, 200)
。这个动画会在父节点被创建时就自动播放。
<sprite file="Image/logo.png">
<loop>
<move time={0.5} startX={0} startY={0} stopX={200} stopY={200}/>
<move time={0.5} startX={200} startY={200} stopX={0} stopY={0}/>
</loop>
</sprite>
可以使用一个循环播放动作的标签 <loop>
,使子元素的动作序列循环播放。如果需要创建只播放一次的动作序列,可以改成 <sequence>
标签。如果需要同时播放多个动 作,可以使用 <spawn>
标签进行嵌套。注意 <loop>
、<sequence>
和 <spawn>
标签只能包含动作元素,并且 <loop>
标签只能作为嵌套的最外层标签使用,<sequence>
和 <spawn>
标签则可以任意组合。
如果你想创建一个稍后使用的动作序列,可以在最外层使用 <action>
标签。
import { ActionDef, Sprite } from "Dora";
import { React, toNode, useRef } from "DoraX";
// 创建一个包含图元精灵和动作序列的函数组件
const ActionNode = () => {
// 创建引用
const spriteRef = useRef<Sprite.Type>();
const actionRef = useRef<ActionDef.Type>();
// 在点击事件处理函数中播放动作
const onTapped = () => {
const {current: sprite} = spriteRef;
const {current: action} = actionRef;
if (sprite && action && sprite.actionCount === 0) {
sprite.perform(action);
}
};
// 返回图元精灵和稍后再使用的动作序列
return (
<sprite ref={spriteRef} file="Image/logo.png" onTapped={onTapped}>
<action ref={actionRef}>
<sequence>
<move time={0.5} startX={0} startY={0} stopX={200} stopY={200}/>
<move time={0.5} startX={200} startY={200} stopX={0} stopY={0}/>
</sequence>
</action>
</sprite>
);
};
// 实例化组件
toNode(<ActionNode/>);
在上面的示例中,我们创建了一个包含精灵和动作序列的组件 ActionNode
,并在点击事件中播放动作序列。
7.2 目前支持的动作元素
目前 Dora SSR 支持以下动作元素:
<action>
:创建一个可被引用的动作序列。<anchor-x>
:持续改变节点的 X 锚点。<anchor-y>
:持续改变节点的 Y 锚点。<angle>
:持续改变节点的角度(Z 轴)。<angle-x>
:持续改变节点的 X 轴旋转角度。<angle-y>
:持续改变节点的 Y 轴旋转角度。<delay>
:在动画时间线中产生延迟。<event>
:触发事件。<width>
:持续改变节点的宽度。<height>
:持续改变节点的高度。<hide>
:隐藏节点。<show>
:显示节点。<move>
:持续改变节点的 X、Y 坐标位置。<move-x>
:持续改变节点的 X 坐标位置。<move-y>
:持续改变节点的 Y 坐标位置。<move-z>
:持续改变节点的 Z 位置。<opacity>
:持续改变节点的不透明度。<roll>
:持续改变节点的旋转。<scale>
:持续改变节点的 X 和 Y 轴缩放。<scale-x>
:持续改变节点的 X 轴缩放。<scale-y>
:持续改变节点的 Y 轴缩放。<skew-x>
:持续改变节点的 X 轴倾斜。<skew-y>
:持续改变节点的 Y 轴倾斜。<frame>
:创建帧动画。<loop>
:重复执行动作。<spawn>
:并行执行一组动作。<sequence>
:顺序执行一系列动作。
7.3 描述性的元素
除了动作元素,Dora SSR 还提供了一些描述性的元素,用于进一步描述要创建的游戏对象的外观和行为。
<physics-world>
<contact groupA={0} groupB={1} enabled={false}/> {/* 定义物理分组碰撞关系 */}
<body type={BodyMoveType.Dynamic} group={0}>
<disk-fixture radius={50}/> {/* 定义物理体的碰撞形状 */}
<draw-node>
<dot-shape radius={50}/> {/* 定义绘制节点的形状 */}
</draw-node>
</body>
<effek-node>
<effek file='Particle/effek/Laser01.efk'/> {/* 定义播放特效的信息 */}
</effek-node>
</physics-world>
在上面的示例中,我们使用了一些描述性的元素,包括 <contact>
、<disk-fixture>
、<dot-shape>
和 <effek>
。这些元素不会被实例化为独立的游戏对象,而是用于补充描述要创建的父级元素游戏对象的物理属性、碰撞形状、绘制形状和特效信息。
7.4 目前支持的描述性元素
目前 Dora SSR 支持以下描述性元素:
-
可以在
<draw-node>
下使用的绘制形状元素:<dot-shape>
:绘制一个点或填充的圆。<segment-shape>
:绘制一条线段。<polygon-shape>
:绘制一个多边形。<rect-shape>
:绘制一个矩形。<verts-shape>
:绘制一个多边形,每个顶点有自己的颜色。
-
可以在
<body>
下使用的碰撞形状元素:<rect-fixture>
:定义一个矩形形状的碰撞体。<polygon-fixture>
:定义一个多边形形状的碰撞体。<multi-fixture>
:定义一个由多个凸边形组成的凹边形碰撞体。<disk-fixture>
:定义一个圆盘形状的碰撞体。<chain-fixture>
:定义一个线段组成链形状的碰撞体。
7.5 使用 <custom-node>
创建自定义节点
如果你需要创建一个自定义的游戏节点,可以使用 <custom-node>
元素。这个元素允许你复用一些使用 TSX 以外的代码编写的,创建其它游戏对象的程序模块。并把这些代码封装为新的 TSX 组件。
// 引入一个非 TSX 代码编写的按钮组件
import * as ButtonCreate from 'UI/Control/Basic/Button';
import { Button } from 'UI/Control/Basic/Button';
// 定义新的 TSX 按钮组件的属性
interface ButtonProps {
ref?: JSX.Ref<Button.Type>;
text: string;
width: number;
height: number;
onClick?: () => void;
}
// 使用 `<custom-node>` 创建新的 TSX 按钮组件
// 并复用导入的外部组件的代码
const Button = (props: ButtonProps) => {
return <custom-node onCreate={() => {
const button = ButtonCreate({
text: props.text,
width: props.width,
height: props.height
});
button.onTapped(() => {
if (props.onClick) {
props.onClick();
}
});
if (props.ref) {
(props.ref.current as any) = button;
}
return button;
}}/>;
};
// 使用新的 TSX 按钮组件
toNode(
<Button text="Button" width={60} height={60}/>
);
在上面的示例中,我们使用 <custom-node>
元素创建了一个新的 TSX 按钮组件,复用了外部导入的非 TSX 代码编写的按钮组件代码。这样,你可以在 Dora SSR 中使用自定义的游戏节点,扩展 TSX 语言的功能来创建游戏对象。